using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Castle.Components.Validator;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.QueryParsers;
using Newtonsoft.Json.Linq;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.SqlCommand;
using Rhino.Commons;
using WineCellar.Core.Interfaces;
using WineCellar.Core.NHibernate;
using NHSearch = NHibernate.Search;
using ValidationException=Rhino.Security.Exceptions.ValidationException;


namespace WineCellar.Core.Services
{
    public abstract class EntityService<T> : IEntityService<T>        
    {
        public virtual T Get(Guid id)
        {
            return Repository<T>.Get(id);
        }

        public virtual ICollection<T> GetAll()
        {
            return Repository<T>.FindAll();
        }

        public FutureQueryOf<T> GetAll(int firstResult, int numberOfResults,
                                       string orderBy, string sortOrder)
        {
            return GetAll(firstResult, numberOfResults, orderBy, sortOrder, null);
        }

        public FutureQueryOf<T> GetAll(int firstResult, int numberOfResults,
                                       string orderBy, string sortOrder, IEnumerable<string> eagerLoadingFor)
        {
            Order order;
            var criteria = GetOrderByCriteriaForGetAll(sortOrder, orderBy, out order, eagerLoadingFor);
            return new FutureQueryOf<T>(criteria, firstResult, numberOfResults);
        }

        public FutureQueryOf<T> Search(ISearchObject searchObject, int start, int limit,
                                       string sort, string dir, IEnumerable<string> eagerLoadingFor)
        {
            var criteria = GetCriteriaForSearch(dir, sort, searchObject, eagerLoadingFor);
            return new FutureQueryOf<T>(criteria, start, limit);
        }

        public FutureQueryOf<T> Search(ISearchObject searchObject, int start, int limit,
                                       string sort, string dir)
        {
            return Search(searchObject, start, limit, sort, dir, null);
        }

        public virtual void Remove(T instance)
        {
            Repository<T>.Delete(instance);
        }

        public virtual void Save(T instance)
        {
            var validator = new ValidatorRunner(new CachedValidationRegistry());
            if (validator.IsValid(instance))
            {
                Repository<T>.Save(instance);    
            }
            else
            {
                throw new ValidationException(validator.GetErrorSummary(instance));
            }            
            
        }

        public bool IsUpdate(Guid id)
        {
            return Repository<T>.Get(id) != null;
        }

        /// <summary>
        /// Performs a Lucene search with the given searchstring in the given fields (tokens)
        /// </summary>
        /// <param name="searchString">the string to search for</param>
        /// <param name="tokens">the fields to search in</param>
        /// <returns></returns>
        public ICollection<T> LuceneSearch(string searchString, IEnumerable<JToken> tokens)
        {
            return LuceneSearch(searchString, tokens, null, null);
        }

        /// <summary>
        /// Performs a Lucene search with the given searchstring in the given fields (tokens)
        /// </summary>
        /// <param name="searchString">the string to search for</param>
        /// <param name="tokens">the fields to search in</param>
        /// <param name="restrictionSearchString">restriction search term. Should be an id to be good.</param>
        /// <param name="restrictionToken">field to apply the restriction to</param>
        /// <returns></returns>
        public ICollection<T> LuceneSearch(string searchString, IEnumerable<JToken> tokens,
                                           string restrictionSearchString, string restrictionToken)
        {
            // Create a Full Text session
            var fullTextSession = NHSearch.Search.CreateFullTextSession(UnitOfWork.CurrentSession);

            // Build our Lucene query
            var luceneQuery = ParseLuceneQuery(searchString, tokens, restrictionSearchString, restrictionToken);

            // Transform the Lucene query to an NHibernate query,
            // and limit the result set types to T
            IQuery query = fullTextSession.CreateFullTextQuery(luceneQuery, new[] { typeof(T) });

            // List our results
            return query.List<T>();
        }

       
        /// <summary>
        ///Creates a Lucene query based on searchterms and an array of fields in which to search
        /// </summary>
        /// <param name="searchTerms">search term</param>
        /// <param name="tokens">fields to search in</param>
        /// <param name="restrictionTerm">restrict result set with these term</param>
        /// <param name="restrictionToken">restriction field</param>
        /// <returns>Lucene query</returns>        
        private Lucene.Net.Search.Query ParseLuceneQuery(string searchTerms, IEnumerable<JToken> tokens
                                                         , string restrictionTerm, string restrictionToken)
        {
            var queryParts = new List<string>();
            var queryString = new StringBuilder();
            foreach (var token in tokens)
            {
                if (!String.IsNullOrEmpty(token.ToString()))
                {
                    queryString = new StringBuilder();
                    var field = token.ToString().Remove(0, 1);
                    field = field.Remove(field.Length - 1, 1);
                    queryString.Append(field.Trim());
                    queryString.Append(":");
                    queryString.Append(searchTerms.Trim());
                    queryString.Append("*");
                    queryParts.Add(queryString.ToString());
                }
            }
            var query = string.Join(" or ", queryParts.ToArray());
            var restrictionQueryString = new StringBuilder();
            if (!String.IsNullOrEmpty(restrictionToken))
            {
                restrictionQueryString.Append(restrictionToken.Trim());
                restrictionQueryString.Append(":");
                restrictionQueryString.Append(restrictionTerm.Trim());
                query = restrictionQueryString + " AND " + query;
            }

            var parser = new QueryParser(query, new StandardAnalyzer());

            return parser.Parse(query);
        }

        /// <summary>
        /// Gets the order by criteria for a GetAll query
        /// </summary>
        /// <param name="sortOrder">order by direction</param>
        /// <param name="orderBy">order by string</param>
        /// <param name="order">out Ordery object</param>
        /// <param name="eagerLoadingFor">associations that should be eagerly loaded</param>
        /// <returns>DetachedCriteria</returns>
        protected DetachedCriteria GetOrderByCriteriaForGetAll(string sortOrder, string orderBy,
                                                               out Order order, IEnumerable<string> eagerLoadingFor)
        {
            var ascending = (sortOrder == "ASC") ? true : false;
            var criteria = DetachedCriteria.For<T>();
            //add eager loading
            if (eagerLoadingFor != null)
            {
                foreach (var fetchmode in eagerLoadingFor)
                {
                    criteria = criteria.SetFetchMode(fetchmode, FetchMode.Eager);
                }
            }
            char[] splitter = { '.' };
            var splitOrder = orderBy.Split(splitter);
            for (var i = 0; i < splitOrder.Length - 1; i++)
            {
                criteria = criteria.CreateCriteria(splitOrder[i], JoinType.LeftOuterJoin);
            }
            order = new Order(splitOrder[splitOrder.Length - 1], ascending);
            criteria.AddOrder(order);
            return criteria;
        }

        /// <summary>
        /// Gets the detachedcriteria for a search query used by the grid to do its filtering
        /// </summary>
        /// <param name="dir">order by direction</param>
        /// <param name="sort">order by string (can be separated by dots: Address.Country.Name)</param>
        /// <param name="searchObject">searchObject</param>
        /// <param name="eagerLoadingFor">the associations for which eager loading has to be performed</param>
        /// <returns>DetachedCriteria</returns>
        protected DetachedCriteria GetCriteriaForSearch(string dir, string sort, ISearchObject searchObject, IEnumerable<string> eagerLoadingFor)
        {
            var searchParameters = searchObject.GetParameters();
            var searchCriteria = DetachedCriteria.For(typeof(T), "company");
            var ascending = (dir == "ASC");
            var orderAdded = false;
            //add eager loading options
            if (eagerLoadingFor != null)
            {
                foreach (var fetchmode in eagerLoadingFor)
                {
                    searchCriteria = searchCriteria.SetFetchMode(fetchmode, FetchMode.Eager);
                }
            }
            foreach (var parameter in searchParameters)
            {
                searchCriteria = searchCriteria.GetCriteriaByAlias("company");
                char[] splitter = { '.' };
                var splitStrings = parameter.Column.Split(splitter);
                var stringWithoutDot = parameter.Column.Replace(".", "");
                var prop = GetProperty(searchObject, stringWithoutDot, parameter.Method);
                //add search parameters
                if (prop != null)
                {
                    if (sort == null)
                    {
                        sort = String.Empty;
                    }
                    var splitOrder = sort.Split(splitter);
                    //if order and filter contain association - check whether the first one is the same
                    //only add the order if it will not be added by other criteria, otherwise we'll get duplicates
                    if (!orderAdded && splitOrder.Length > 1 && splitStrings.Length > 1 && splitOrder[0] != splitStrings[0])
                    {
                        var addOrderHere = true;
                        foreach (var param in searchParameters)
                        {
                            if (param.Column.Contains(splitOrder[0]))
                            {
                                //we find the association in another searchparam, so we don't add the order here
                                //because it will be added when the criteria for that search param are created.
                                addOrderHere = false;
                            }
                        }
                        if (addOrderHere)
                        {
                            searchCriteria.CreateCriteria(splitOrder[0], JoinType.LeftOuterJoin).AddOrder(new Order(splitOrder[1], ascending));
                            orderAdded = true;
                        }
                    }
                    for (var i = 0; i < splitStrings.Length - 1; i++)
                    {
                        if (!orderAdded && splitOrder.Length == 1 && i == 0)
                        {
                            if (!String.IsNullOrEmpty(splitOrder[0]))
                            {
                                searchCriteria.AddOrder(new Order(splitOrder[0], ascending));
                            }
                            orderAdded = true; //no order specified no need to continue further
                        }
                        //only create criteria if they are not already created
                        searchCriteria = searchCriteria.GetCriteriaByAlias(splitStrings[i]) ??
                                         searchCriteria.CreateCriteria(splitStrings[i], splitStrings[i]);
                        if (!orderAdded && splitOrder[splitOrder.Length - 2] == splitStrings[i])
                        {
                            if (!String.IsNullOrEmpty(splitOrder[splitOrder.Length - 1]))
                            {
                                searchCriteria.AddOrder(new Order(splitOrder[splitOrder.Length - 1], ascending));
                                orderAdded = true;
                            }
                        }
                    }
                    var criterion = NHibernateHelper.TranslateSearchParametersToNHibernateCriterion(
                        splitStrings[splitStrings.Length - 1], prop.GetValue(searchObject, null), parameter.Method);
                    searchCriteria.Add(criterion);
                }
            }
            return searchCriteria;
        }

        private PropertyInfo GetProperty(ISearchObject searchObject, string stringWithoutDot, string method)
        {
            var prop = searchObject.GetType().GetProperty(stringWithoutDot, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
            //find the numeric and date properties as well.
            if (prop == null)
            {
                stringWithoutDot = stringWithoutDot + method;
                prop = searchObject.GetType().GetProperty(stringWithoutDot, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
            }
            return prop;
        }
    }
}